
// Copyright (C) 2012 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//  
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Xml.Serialization;

namespace Derm.Xml.Collada
{
	/// <summary>
	/// Specific class for serialize/deserialize COLLADA XML elements and attributes.
	/// </summary>
	/// <remarks>
	/// <para>
	/// This class extends <see cref="XmlSerializableExt"/> in order to facilitate the XML parsing by implementing the
	/// interface <see cref="IXmlSerializable"/>.
	/// </para>
	/// <para>
	/// This class introduce XML parent relationship used for navigating the XML document programmatically.
	/// </para>
	/// <para>
	/// Because the parent relationship is defined, it's possible to get the originating <see cref="ColladaDocument"/>
	/// instance since it is always the root parent.
	/// </para>
	/// </remarks>
	public class ColladaXmlSerializable : XmlSerializableExt
	{
		#region Constructors

		/// <summary>
		/// Construct a ColladaXmlSerializable specifying its parent element.
		/// </summary>
		public ColladaXmlSerializable(ColladaXmlSerializable parent)
		{
			mParent = parent;
		}

		#endregion

		#region Parent Relationship

		/// <summary>
		/// 
		/// </summary>
		/// <param name="node"></param>
		/// <returns></returns>
		public delegate bool ColladaNodeFilterDelegate(ColladaXmlSerializable node);

		/// <summary>
		/// 
		/// </summary>
		/// <param name="filter"></param>
		/// <returns></returns>
		public ColladaXmlSerializable GetParent(ColladaNodeFilterDelegate filter)
		{
			ColladaXmlSerializable parent = mParent;

			while ((parent != null) && (filter(parent) == false))
				parent = parent.mParent;

			return (parent);
		}

		/// <summary>
		/// The parent element.
		/// </summary>
		[XmlIgnore]
		public ColladaXmlSerializable Parent { get { return (mParent ?? this); } }

		/// <summary>
		/// Determine whether this COLLADA serializable is the root element.
		/// </summary>
		[XmlIgnore]
		public bool IsRoot { get { return (mParent == null); } }

		/// <summary>
		/// The parent element.
		/// </summary>
		private readonly ColladaXmlSerializable mParent;

		#endregion

		#region Document Relationship

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public ColladaDocument GetColladaDocument()
		{
			ColladaXmlSerializable cursor = this;

			// While there is a parent defined
			while (cursor.IsRoot == false)
				cursor = cursor.Parent;

			Debug.Assert(cursor.GetType() == typeof(ColladaDocument));

			return ((ColladaDocument)cursor);
		}

		#endregion

		#region Reference Relations

		/// <summary>
		/// 
		/// </summary>
		/// <param name="url"></param>
		/// <returns></returns>
		internal static string GetFragmentName(string url)
		{
			if (url == null)
				throw new ArgumentNullException("url");
			if (url.StartsWith("#") == false)
				throw new ArgumentException("invalid", "url");

			return (url.Substring(1));
		}

		/// <summary>
		/// Resolve all references specified by this instance.
		/// </summary>
		/// <param name="document">
		/// A <see cref="ColladaDocument"/> used for resolving internal references.
		/// </param>
		/// <exception cref="ArgumentNullException">
		/// Exception thrown if <paramref name="document"/> is null.
		/// </exception>
		/// <remarks>
		/// <para>
		/// This method scans all fields/properties of this <see cref="ColladaXmlSerializable"/> instance for resolving
		/// COLLADA references automatically.
		/// </para>
		/// <para>
		/// The following elements are ignored:
		/// - Non-public fields and properties
		/// - Static fields and properties
		/// - Field and properties having a <see cref="XmlIgnoreAttribute"/> attribute
		/// - Properties with no getter method (write-only properties)
		/// - Properties with indexed parameters (indexers)
		/// </para>
		/// <para>
		/// All fields and properties having a null value are not inspected for reference resolution. Fields and properties
		/// referencing this <see cref="ColladaXmlSerializable"/> are ignored automatically to avoid stack overflows.
		/// </para>
		/// <para>
		/// If a field or property is considered and its value is a <see cref="IColladaInstance"/>, the reference is resolved
		/// by calling <see cref="IColladaInstance.Resolve"/> on the value.
		/// </para>
		/// <para>
		/// If a field or property is considered and its value is a <see cref="ColladaXmlSerializable"/> instance, the method
		/// recurse on that value.
		/// </para>
		/// <para>
		/// If a field or property is considered and its value is a <see cref="IEnumerable"/> instance, the method recurse on
		/// the collected values using the same algorithm.
		/// </para>
		/// <para>
		/// Note that a considered value can be a <see cref="IColladaInstance"/>, a <see cref="ColladaXmlSerializable"/> and
		/// a <see cref="IEnumerable"/> at the same time.
		/// </para>
		/// </remarks>
		public void ResolveReferences(ColladaDocument document)
		{
			Type objType = GetType();
			List<object> colladaObjects = new List<object>();

			if (document == null)
				throw new ArgumentNullException("document");

			// Get fields value
			foreach (FieldInfo fieldInfo in objType.GetFields(BindingFlags.Public | BindingFlags.Instance)) {
				if (Attribute.GetCustomAttribute(fieldInfo, typeof(XmlIgnoreAttribute)) == null) {
					object value = fieldInfo.GetValue(this);

					if ((value != null) && (ReferenceEquals(value, this) == false))
						colladaObjects.Add(value);
				}
			}

			// Get property value
			foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) {
				if (propertyInfo.CanRead == false)
					continue;
				if (propertyInfo.GetIndexParameters().Length > 0)
					continue;

				if (Attribute.GetCustomAttribute(propertyInfo, typeof(XmlIgnoreAttribute)) == null) {
					object value = propertyInfo.GetValue(this, null);

					if ((value != null) && (ReferenceEquals(value, this) == false))
						colladaObjects.Add(value);
				}
			}

			// Resolve references
			foreach (object memberValue in colladaObjects)
				ResolveReferences(document, memberValue);
		}

		/// <summary>
		/// Resolve all references specified by an object.
		/// </summary>
		/// <param name="document">
		/// A <see cref="ColladaDocument"/> used for resolving internal references.
		/// </param>
		/// <param name="value">
		/// A <see cref="System.Object"/> that specify the object to resolve.
		/// </param>
		/// <exception cref="ArgumentNullException">
		/// Exception thrown if <paramref name="document"/> or <paramref name="value"/> are null.
		/// </exception>
		private void ResolveReferences(ColladaDocument document, object value)
		{
			if (document == null)
				throw new ArgumentNullException("document");
			if (value == null)
				throw new ArgumentNullException("value");

			Type valueType = value.GetType();

			// Resolve the reference of a IColladaInstance implementation
			if (valueType.GetInterface("IColladaInstance") != null) {
				IColladaInstance colladaInstance = (IColladaInstance) value;

				// Resolve the specific COLLADA instance
				colladaInstance.Resolve(document);
			}

			// Resolve references of the object fields if derives from ColladaXmlSerializable (mostly true for all classes)
			if (valueType.IsSubclassOf(typeof(ColladaXmlSerializable))) {
				ColladaXmlSerializable colladaXmlSerializable = (ColladaXmlSerializable) value;

				// Resolve recursively all ColladaXmlSerializable members
				colladaXmlSerializable.ResolveReferences(document);
			}
			
			// If it is an enumeration, enumerate values and recurse
			if (valueType.GetInterface("IEnumerable") != null) {
				IEnumerable colladaEnumerable = (IEnumerable) value;

				foreach (object colladaEnumerated in colladaEnumerable) {
					if (colladaEnumerated != null)
						ResolveReferences(document, colladaEnumerated);
				}
			}
		}

		#endregion
	}
}
